/*
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.skatiger.jrove.spring.view;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.Writer;
import java.util.Map;

import javax.el.ELException;
import javax.servlet.ServletContext;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.skatiger.jrove.Configurator;
import org.skatiger.jrove.ExternalContext;
import org.skatiger.jrove.Jrove;
import org.skatiger.jrove.JroveException;
import org.skatiger.jrove.ResponseWriter;
import org.skatiger.jrove.ViewContext;
import org.skatiger.jrove.component.UIViewRoot;
import org.skatiger.jrove.config.ConfigProperties;
import org.skatiger.jrove.impl.DefaultConfigurator;
import org.skatiger.jrove.impl.DefaultViewContext;
import org.skatiger.jrove.impl.HtmlResponseWriterImpl;
import org.skatiger.jrove.servlet.ServletExternalContext;
import org.skatiger.jrove.util.AssertUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.web.servlet.support.RequestContext;
import org.springframework.web.servlet.view.AbstractTemplateView;

public class JroveView extends AbstractTemplateView {
    protected final static Logger logger = LoggerFactory
            .getLogger(JroveView.class);
    /**
     * {@link ViewContext} attribute for the view-level {@link RequestContext}
     * instance.
     */
    public static final String REQUEST_CONTEXT_VIEW_ATTRIBUTE = "org.skatiger.jrove.spring.~REQUEST_CONTEXT~";

    protected static class NullWriter extends Writer {
        static final NullWriter Instance = new NullWriter();

        @Override
        public void close() {
        }

        @Override
        public void flush() {
        }

        @Override
        public void write(char[] buffer) {
        }

        @Override
        public void write(char[] buffer, int off, int len) {
        }

        @Override
        public void write(int c) {
        }

        @Override
        public void write(String str) {
        }

        @Override
        public void write(String str, int off, int len) {
        }
    }

    private JroveConfig jroveConfig;

    protected JroveConfig autodetectConfig() throws BeansException {
        if (this.jroveConfig == null) {
            this.jroveConfig = (JroveConfig) BeanFactoryUtils
                    .beanOfTypeIncludingAncestors(getApplicationContext(),
                            JroveConfig.class, true, false);
        }
        AssertUtil
                .notNull(
                        this.jroveConfig,
                        "Must define a single JroveConfig bean in this application context "
                                + "(may be inherited): JroveConfigurer is the usual implementation. This bean may be given any name.");

        return this.jroveConfig;
    }

    protected ResponseWriter createResponseWriter(ViewContext context) throws IOException,
            JroveException {
        Configurator config = autodetectConfig().getConfigurator();
        ExternalContext extContext = context.getExternalContext();
        ServletResponse response = (ServletResponse) extContext.getResponse();

        // set the buffer for content
        int bufferSize = config.getProperty(ConfigProperties.BUFFER_SIZE);
        if (bufferSize != -1) {
            response.setBufferSize(bufferSize);
        }
        // get our content type
        String contentType = "text/html";

        // get the encoding
        String encoding = context.getViewRoot().getEncoding();
        ResponseWriter writer;
        // append */* to the contentType so createResponseWriter will succeed no
        // matter the requested contentType.
        if (contentType != null && !contentType.equals("*/*")) {
            contentType += ",*/*";
        }
        // Create a dummy ResponseWriter with a bogus writer,
        // so we can figure out what content type the ReponseWriter
        // is really going to ask for
        writer = new HtmlResponseWriterImpl(NullWriter.Instance, contentType,
                encoding);
        // contentType = getResponseContentType(context,
        // writer.getContentType());
        // encoding = getResponseEncoding(context,
        // writer.getCharacterEncoding());
        // apply them to the response
        String _contentType = contentType + "; charset=" + encoding;
        response.setContentType(_contentType);
        // Now, clone with the real writer
        writer = writer.cloneWithWriter(response.getWriter());

        return writer;
    }

    protected UIViewRoot createView(ViewContext context, String viewId) {
        UIViewRoot viewRoot = new UIViewRoot(viewId);
        viewRoot.setParent(null);

        context.setViewRoot(viewRoot);// set current viewRoot in ViewContext

        return viewRoot;
    }

    private void buildJroveView(ViewContext context, UIViewRoot viewToRender) throws IOException,
            JroveException {

        if (logger.isDebugEnabled()) {
            logger.debug("Building View: " + viewToRender.getViewId());
        }

        Jrove j = autodetectConfig().getJroveFactory().getJrove(
                viewToRender.getViewId());
        // populate UIViewRoot
        long time = System.currentTimeMillis();
        j.apply(context, viewToRender);
        time = System.currentTimeMillis() - time;
        if (logger.isDebugEnabled()) {
            logger.debug("Took " + time + "ms to build view: "
                    + viewToRender.getViewId());
        }
    }

    @Override
    protected void renderMergedTemplateModel(Map<String, Object> model,
            HttpServletRequest request,
            HttpServletResponse response) throws Exception {
        DefaultConfigurator config = (DefaultConfigurator) autodetectConfig()
                .getConfigurator();
        ExternalContext externalContext = new ServletExternalContext(
                getServletContext(), request, response);
        ViewContext viewContext = config.createViewContext(externalContext);
        exposeRequestContextAsViewContextAttribute(model, viewContext);
        exposeModelAsViewContextAttributes(model, viewContext);
        doRender(viewContext);
        viewContext.release();
    }

    protected void doRender(ViewContext viewContext) throws Exception {
        String url = getUrl();
        UIViewRoot viewroot = createView(viewContext, url);
        renderView(viewContext, viewroot);
        if (logger.isDebugEnabled()) {
            if (viewContext.isCompleted()) {
                logger.debug("renderer viewroot completed");
            }
        }
    }

    protected void exposeRequestContextAsViewContextAttribute(Map<String, Object> model,
            ViewContext viewContext) {
        ExternalContext extctx = viewContext.getExternalContext();
        HttpServletRequest request = (HttpServletRequest) extctx.getRequest();
        HttpServletResponse response = (HttpServletResponse) extctx
                .getResponse();
        ServletContext servletContext = (ServletContext) extctx.getContext();
        viewContext.setAttribute(REQUEST_CONTEXT_VIEW_ATTRIBUTE,
                new RequestContext(request, response, servletContext, model));
    }

    protected void exposeModelAsViewContextAttributes(Map<String, Object> model,
            ViewContext context) {

        for (Map.Entry<String, Object> entry : model.entrySet()) {
            String modelName = entry.getKey();
            Object modelValue = entry.getValue();
            if (modelValue != null) {
                context.setAttribute(modelName, modelValue);
                if (logger.isDebugEnabled()) {
                    logger.debug("Added model object '" + modelName
                            + "' of type [" + modelValue.getClass().getName()
                            + "] to request in view with name '"
                            + getBeanName() + "'");
                }
            } else {
                context.removeAttribute(modelName);
                if (logger.isDebugEnabled()) {
                    logger.debug("Removed model object '" + modelName
                            + "' from request in view with name '"
                            + getBeanName() + "'");
                }
            }
        }
    }

    protected void renderView(ViewContext context, UIViewRoot viewToRender) throws IOException,
            JroveException {
        // exit if the view is not to be rendered
        if (!viewToRender.isRendered()) {
            return;
        }

        // logger request
        if (logger.isDebugEnabled()) {
            logger.debug("Rendering View: " + viewToRender.getViewId());
        }
        try {
            // build view - but not if we're in "buildBeforeRestore"
            // land and we've already got a populated view. Note
            // that this optimizations breaks if there's a "c:if" in
            // the page that toggles as a result of request processing -
            // should that be handled? Or
            // is this optimization simply so minor that it should just
            // be trimmed altogether?
            if (viewToRender.getChildren().isEmpty()) {
                this.buildJroveView(context, viewToRender);
            }

            // setup writer and assign it to the context
            ResponseWriter writer = this.createResponseWriter(context);
            // QUESTION: should we use bufferSize? Or, since the
            // should we always use a much smaller size?
            // ResponseWriter writer = origWriter.cloneWithWriter(origWriter);
            context.setResponseWriter(writer);

            context.getExternalContext().getSession(true);
            long time = System.currentTimeMillis();

            // render the view to the response
            writer.startDocument();

            viewToRender.encodeAll(context);

            writer.endDocument();
            // finish writing
            writer.close();

            time = System.currentTimeMillis() - time;
            if (logger.isDebugEnabled()) {
                logger.debug("Took " + time + "ms to render view: "
                        + viewToRender.getViewId());
            }
            context.complete();
        } catch (FileNotFoundException fnfe) {
            this.handleNotFound(context, viewToRender.getViewId());
        } catch (Exception e) {
            this.handleRenderException(context, e);
        } finally {
        }
    }

    // process exception
    protected void handleNotFound(ViewContext context, String viewId) throws JroveException,
            IOException {
        Object respObj = context.getExternalContext().getResponse();
        if (respObj instanceof HttpServletResponse) {
            HttpServletResponse respHttp = (HttpServletResponse) respObj;
            respHttp.sendError(HttpServletResponse.SC_NOT_FOUND, viewId);
            context.complete();
        }
    }

    protected void handleRenderException(ViewContext context, Exception e) throws IOException,
            ELException,
            JroveException {
        HttpServletResponse response = (HttpServletResponse) context
                .getExternalContext().getResponse();
        UIViewRoot root = context.getViewRoot();

        // always logger
        if (logger.isErrorEnabled()) {
            StringBuffer sb = new StringBuffer(64);
            sb.append("Error Rendering View");
            if (root != null) {
                sb.append('[');
                sb.append(root.getViewId());
                sb.append(']');
            }
            logger.error(sb.toString(), e);
        }
        response.sendError(
                HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
                "error rendered view " + root.getViewId() + " messages: "
                        + e.getMessage());
        context.complete();
    }

}
